package com.ct.dao.searching.hibernate;

import com.ct.dao.common.SearchDao;
import com.ct.dao.common.ObjectProvider;
import com.ct.dao.searching.Query;
import com.ct.dao.utils.HolderUtil;
import com.ct.model.common.Entity;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Iterator;

import org.hibernate.Session;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.ScrollableResults;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.lang.StringUtils;

/**
 * Created by IntelliJ IDEA.
 * User: nmkravchenko
 * Date: 22.10.2009
 * Time: 18:03:52
 * To change this template use File | Settings | File Templates.
 */
public class SearchDaoImpl  implements SearchDao {
    private final static Log LOG = LogFactory.getLog(SearchDaoImpl.class);
    private final static int MAX_ALLOWABLE_COUNT = 1000;
    private ObjectProvider<Session> _sessionHolder;
    
    public void setSessionHolder(ObjectProvider<Session> aSessionHolder) {
        _sessionHolder = aSessionHolder;
    }

    public <T extends Entity> Collection<T> findByQuery(Query<T> aQuery) {
        return findByQuery(aQuery, 0, -1);
    }

    public <T extends Entity> T findFirst(Query<T> aQuery){
        Collection<T> retCol = findByQuery(aQuery, 0, 1);
        T ret = null;
        if(retCol != null && !retCol.isEmpty()){
            Iterator it = retCol.iterator();
            ret = (T)it.next();
        }
        return ret;
    }

    public <T extends Entity> Collection<T> findByQuery(Query<T> aQuery, int aOffset, int aCount) {
        assertSize(aCount, aQuery);
        HqlQuery query = (HqlQuery) aQuery;
        org.hibernate.Query hibernateQuery = getSession()
                .createQuery(query.getQueryString());
        setParameters(hibernateQuery, query.getParameters());
        if (aOffset > 0) {
            hibernateQuery.setFirstResult(aOffset);
        }
        if (aCount >= 0) {
            hibernateQuery.setMaxResults(aCount);
        }
        hibernateQuery.setCacheable(query.isCacheable());
        hibernateQuery.setCacheRegion(query.getCacheRegion());
        try {
        	CountTransformer tr = null;
        	if (query.isEnabledUseRowCount()) {
        		tr = new CountTransformer();
        		hibernateQuery.setResultTransformer(tr);
        	}
        	List queryResult = hibernateQuery.list();
        	assertSize(queryResult.size(), aQuery);
        	if (queryResult.size() != 0 && query.isEnabledUseRowCount()) {
        		return new ListWithCount(queryResult, tr.getCount());
        	}
      		return queryResult;
        } catch (HibernateException e) {
            if (e instanceof JDBCException) {
                throw new HibernateException(
                        "Failed to execute query[" + ((JDBCException) e).getSQL() + "]",
                        e);
            }
            throw e;
        }
    }
    private void assertSize(int aCount, Query aQuery) {
        assert aCount <= MAX_ALLOWABLE_COUNT : "aCount[" + aCount + "] must be <= "
                + MAX_ALLOWABLE_COUNT + " for query " + aQuery;
    }

    private void setParameters(org.hibernate.Query aQuery, Map<String, Object> aParams) {
        for (String paramName : aParams.keySet()) {
            if (aParams.get(paramName) instanceof Collection) {
                aQuery.setParameterList(paramName, (Collection) aParams.get(paramName));
            } else {
                aQuery.setParameter(paramName, aParams.get(paramName));
            }
        }
    }

    /**
     * Implements multiple count strategies:
     * <ul>
     *  <li>If query is cacheable, then just execute query and get collection size.
     *  <li>Else If count query is set, then just execute this count query and return result.
     *  <li>Else use ScrollableResultSet to move to last result and get index.
     * </ul>
     * @param aQuery based on which get expected size of result collection
     * @return expected size of result collection
     */
    public int getCount(Query aQuery) {
        final HqlQuery query = (HqlQuery) aQuery;
        try {
            if (StringUtils.isNotBlank(query.getCountQuery())) {
                org.hibernate.Query hibernateQuery = !query.isSqlCountQuery()
                        ? getSession().createQuery(query.getCountQuery())
                        : getSession().createSQLQuery(query.getCountQuery());
                setParameters(hibernateQuery, query.getParameters());
                Number result = (Number) hibernateQuery.uniqueResult();
                if (result != null) {
                    return result.intValue();
                }
                if (LOG.isWarnEnabled()) {
                    LOG.warn("Count query is wrong - likely programmer error!["
                            + query.getCountQuery() + "]");
                }
            }
            return fallbackCount(aQuery);
        } catch (HibernateException e) {
            if (e instanceof JDBCException) {
                throw new HibernateException(
                        "Failed to execute query[" + ((JDBCException) e).getSQL() + "]",
                        e);
            }
            throw e;
        }
    }
    private int fallbackCount(Query aQuery) {
        HqlQuery query = (HqlQuery) aQuery;
        org.hibernate.Query hibernateQuery = getSession()
                .createQuery(query.getQueryString());
        setParameters(hibernateQuery, query.getParameters());
        ScrollableResults rs = hibernateQuery.scroll();
        try {
            return rs.last() ? rs.getRowNumber() + 1 : 0;
        } finally {
            rs.close();
        }
    }

    protected Session getSession() {
        return HolderUtil.getRequired(_sessionHolder);
    }    

}
